using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.UI;
using BizElements.BusinessLayer;

namespace BizElements.Web
{
    /// <summary>
    /// Binds <see cref="ISelfValidatingObject"/> and <see cref="IBusinessObject"/> instances to a <see cref="BoundDataGrid"/> and implements advanced editing functionalities.
    /// </summary>
    /// <remarks><para><b>BusinessObjectsGridAdapter</b> binds a list of <b>ISelfValidatingObject</b> instances to a 
    /// <see cref="BoundDataGrid"/> and automates binding and editing logic which is specific for business objects.</para>
    /// <para>The data about new and edited items is kept in session variables unique for the grid which is managed and 
    /// it is maintain between postbacks. All list managamenet such as inserting, editing ant removing objects should be 
    /// executed through adapter so that it can update its sessions variables.</para>
    /// <para><b>BoundDataGrid</b> is a generic control that allows different data-sources so it wouldn't be appropriate
    /// to implement this logic there because errors would occure when working with objects which don't implement
    /// <b>ISelfValidatingObject</b> interface.</para></remarks>
    public sealed class BusinessObjectsGridAdapter
    {
        #region FieldPermissions.

        BoundDataGrid grid;
        string editIndexesSessionKey;
        string insertIndexesSessionKey;
        IList dataSource;
        bool dataIsBound;

        // Parser error descriptions.
        string defaultParseError;
        string integerParseError;
        string numberParseError;
        string datetimeParseError;

        #endregion

        #region Constructors.

        /// <summary>
        /// Initializes a new instance of the <see cref="BusinessObjectsGridAdapter"/> class. Should not be created before Load phase.
        /// </summary>
        /// <param name="grid">Grid used to edit underlying business objects.</param>
        /// <param name="businessObjects">A list of <see cref="IBusinessObject"/> instances used as a data-source for the grid.</param>
        public BusinessObjectsGridAdapter(BoundDataGrid grid, IList businessObjects)
        {
            foreach (ISelfValidatingObject bizObject in businessObjects)
            {
                if (bizObject == null)
                    throw new Exception("Only objects that implement ISelfValidatingObject interface are allowed. Nulls are not allowed.");
            }

            this.grid = grid;
            this.dataSource = businessObjects;
            string gridId = grid.ClientID ?? grid.ID;
            this.editIndexesSessionKey = grid.Page.GetType().FullName + "_GridAdapter_" + gridId + "_EditIndexes";
            this.insertIndexesSessionKey = grid.Page.GetType().FullName + "_GridAdapter_" + gridId + "_InsertIndexes";
            bool isFirstLoad = !grid.Page.IsPostBack;
            if (isFirstLoad)
            {
                this.EditedItemsIndexes = null;
                this.InsertedItemsIndexes = null;
            }

            this.grid.Page.PreRender += new EventHandler(Page_PreRender);
        }

        #endregion

        #region Bind.

        /// <summary>
        /// Bind business objects to grid.
        /// </summary>
        public void DataBind()
        {
            this.grid.DataSource = this.dataSource;
            this.grid.DataBind();
            this.dataIsBound = true;
        }

        private void Page_PreRender(object sender, System.EventArgs e)
        {
            // Ensure that data is bound at least once if datasource is not serialized to view-state.
            if (this.grid.EnableItemViewState == false && !this.dataIsBound)
                DataBind();
        }

        #endregion

        /// <summary>
        /// Gets indexes of all selected business objects in the underlying data source (list).
        /// </summary>
        /// <returns>Array of zero-based indexes.</returns>
        /// <remarks>When no paging is used the return values is equal to <see cref="BoundDataGrid.SelectedIndexes"/>.
        /// If paging is used then the <see cref="BoundDataGrid.SelectedValues"/> property which contains the object keys
        /// is evaluated.</remarks>
        public int[] GetSelectedBusinessObjectIndexes()
        {
            // If paging is not used the method returns indexes of rows as reported by grid, otherwise it
            bool noPaging = !this.grid.AllowPaging || (this.grid.PageSize <= 0);
            if (noPaging)
            {
                return this.grid.SelectedIndexes;
            }
            else
            {
                if (string.IsNullOrEmpty(this.grid.DataKeyField))
                    return new int[0];
                
                string[] selectedKeys = this.grid.SelectedValues;
                List<int> selectedDataItemIndexes = new List<int>(selectedKeys.Length);
                for (int idxData = 0; idxData < this.dataSource.Count; idxData++)
                {
                    string currKey = Convert.ToString(DataBinder.Eval(this.dataSource[idxData], this.grid.DataKeyField));
                    bool isSelected = ArrayUtil.Contains<string>(selectedKeys, currKey);
                    if (isSelected)
                        selectedDataItemIndexes.Add(idxData);
                }

                return selectedDataItemIndexes.ToArray();
            }
        }

        #region Edit.

        /// <summary>
        /// Indexes of items being edited.
        /// </summary>
        public int[] EditedItemsIndexes
        {
            get
            {
                int[] indexes = HttpContext.Current.Session[this.editIndexesSessionKey] as int[];
                if (indexes != null)
                    return indexes;
                else
                    return new int[0];
            }
            private set
            {
                HttpContext.Current.Session[this.editIndexesSessionKey] = value;

                int editCount = (value != null) ? value.Length : 0;
                int editPlusInsertCount = editCount + this.InsertedItemsIndexes.Length;

                bool setGridEditItemIndex = (editCount == 1 && editPlusInsertCount == 1 
                    && this.grid.RowSelectMode == BoundDataGrid.GridRowSelectMode.SingleRow);
                if (setGridEditItemIndex)
                {
                    if (this.grid.AllowPaging && this.grid.PageSize > 0)
                        this.grid.EditItemIndex = value[0] % this.grid.PageSize;
                    else
                        this.grid.EditItemIndex = value[0];
                }
                else
                {
                    this.grid.EditItemIndex = -1;
                }
            }
        }

        /// <summary>
        /// Gets the value which indicates if items are currenty being edited.
        /// </summary>
        public bool IsEditing
        {
            get
            {
                int[] editIndices = this.EditedItemsIndexes;
                bool isEditing = (editIndices != null && editIndices.Length > 0);
                return isEditing;
            }
        }

        /// <summary>
        /// Edits all selected items.
        /// </summary>
        public void EditSelected()
        {
            int[] rowIndexes = GetSelectedBusinessObjectIndexes();
            if (!ArrayUtil.IsNullOrEmpty(rowIndexes))
                Edit(rowIndexes);
        }

        /// <summary>
        /// Edit specified items.
        /// </summary>
        /// <param name="itemIndexesToEdit">Indexes of items to edit.</param>
        public void Edit(params int[] itemIndexesToEdit)
        {
            foreach (ISelfValidatingObject bizObject in this.dataSource)
                bizObject.IsReadOnly = true;

            foreach (int itemIdx in itemIndexesToEdit)
            {
                ISelfValidatingObject editedBizObject = this.dataSource[itemIdx] as ISelfValidatingObject;
                editedBizObject.IsReadOnly = false;
            }

            List<int> editableWithoutInsertedItems = new List<int>();
            int[] insertedItems = this.InsertedItemsIndexes;
            foreach (int itemToModify in itemIndexesToEdit)
            {
                bool isInserted = ArrayUtil.Contains<int>(insertedItems, itemToModify);
                if (!isInserted)
                    editableWithoutInsertedItems.Add(itemToModify);
            }

            this.EditedItemsIndexes = editableWithoutInsertedItems.ToArray();
            DataBind();
            if (!ArrayUtil.IsNullOrEmpty(itemIndexesToEdit))
            {
                int idxOfItemToFocus = (this.grid.AllowPaging && this.grid.PageSize > 0) ? itemIndexesToEdit[0] % this.grid.PageSize : itemIndexesToEdit[0];
                // Don't automatically select edited items since this interferes with selecting insterted items when in edit-all mode.
                this.grid.FocusItem(idxOfItemToFocus);
            }
        }

        /// <summary>
        /// Edits all bound items.
        /// </summary>
        public void EditAll()
        {
            int[] indexesOfItemsToEdit = new int[this.dataSource.Count];
            for (int itemIdx = 0; itemIdx < indexesOfItemsToEdit.Length; itemIdx++)
                indexesOfItemsToEdit[itemIdx] = itemIdx;

            Edit(indexesOfItemsToEdit);
        }

        #endregion

        #region Insert.

        /// <summary>
        /// Indexes of objects inserted through adapter.
        /// </summary>
        public int[] InsertedItemsIndexes
        {
            get
            {
                int[] indexes = HttpContext.Current.Session[this.insertIndexesSessionKey] as int[];
                if (indexes != null)
                    return indexes;
                else
                    return new int[0];
            }
            private set 
            {
                HttpContext.Current.Session[this.insertIndexesSessionKey] = value;

                int insertCount = (value != null) ? value.Length : 0;
                int editPlusInsertCount = insertCount + this.EditedItemsIndexes.Length;

                bool setGridEditItemIndex = (insertCount == 1 && editPlusInsertCount == 1
                    && this.grid.RowSelectMode == BoundDataGrid.GridRowSelectMode.SingleRow);
                if (setGridEditItemIndex)
                {
                    if (this.grid.AllowPaging && this.grid.PageSize > 0)
                        this.grid.EditItemIndex = value[0] % this.grid.PageSize;
                    else
                        this.grid.EditItemIndex = value[0];
                }
                else
                {
                    this.grid.EditItemIndex = -1;
                }
            }
        }

        /// <summary>
        /// Gets the value which indicates if there are new, not accepted, items.
        /// </summary>
        public bool IsInserting
        {
            get
            {
                int[] insertIndices = this.InsertedItemsIndexes;
                bool isInserting = (insertIndices != null && insertIndices.Length > 0);
                return isInserting;
            }
        }

        /// <summary>
        /// Inserts new objects and edits them.
        /// </summary>
        /// <param name="emptyObjects">New empty objects. Ensure that all required but non-editable fields have been 
        /// set to valid values before calling this method.</param>
        public void Insert(params ISelfValidatingObject[] emptyObjects)
        {
            Insert(true, emptyObjects);
        }

        /// <summary>
        /// Inserts new objects and edits them.
        /// </summary>
        /// <param name="rebind">Whether to rebind the changed datasource after the operation is completed.</param>
        /// <param name="emptyObjects">New empty objects. Ensure that all required but non-editable fields have been 
        /// set to valid values before calling this method.</param>
        public void Insert(bool rebind, params ISelfValidatingObject[] emptyObjects)
        {
            if (emptyObjects == null || emptyObjects.Length == 0)
                return;

            int[] insertIndices = new int[emptyObjects.Length];
            for (int newObjIdx = 0; newObjIdx < emptyObjects.Length; newObjIdx++)
            {
                emptyObjects[newObjIdx].IsReadOnly = false;
                insertIndices[newObjIdx] = this.dataSource.Add(emptyObjects[newObjIdx]);
            }

            this.InsertedItemsIndexes = insertIndices;
            if (rebind)
            {
                DataBind();
                if (!ArrayUtil.IsNullOrEmpty(insertIndices))
                {
                    bool isPagingUsed = (this.grid.AllowPaging && this.grid.PageSize > 0);
                    int idxOfItemToFocus = isPagingUsed ? (insertIndices[0] % this.grid.PageSize) : insertIndices[0];
                    bool autoSelectedIfOnlyItemIsInserted = (insertIndices.Length == 1);
                    this.grid.FocusItem(idxOfItemToFocus, autoSelectedIfOnlyItemIsInserted);
                }
            }
        }

        /// <summary>
        /// Inserts new objects at the specified index and edits them.
        /// </summary>
        /// <param name="index">The zero-based index at which objects should be inserted.</param>
        /// <param name="emptyObjects">New empty objects. Ensure that all required but non-editable fields have been 
        /// set to valid values before calling this method.</param>
        public void InsertAt(int index, params ISelfValidatingObject[] emptyObjects)
        {
            InsertAt(true, index, emptyObjects);
        }

        /// <summary>
        /// Inserts new objects at the specified index and edits them.
        /// </summary>
        /// <param name="rebind">Whether to rebind the changed datasource after the operation is completed.</param>
        /// <param name="index">The zero-based index at which objects should be inserted.</param>
        /// <param name="emptyObjects">New empty objects. Ensure that all required but non-editable fields have been 
        /// set to valid values before calling this method.</param>
        public void InsertAt(bool rebind, int index, params ISelfValidatingObject[] emptyObjects)
        {
            if (emptyObjects == null || emptyObjects.Length == 0)
                return;

            int[] insertIndices = new int[emptyObjects.Length];
            for (int newObjIdx = 0; newObjIdx < emptyObjects.Length; newObjIdx++)
            {
                emptyObjects[newObjIdx].IsReadOnly = false;
                int insertAt = index + newObjIdx;
                insertIndices[newObjIdx] = insertAt;
                this.dataSource.Insert(insertAt, emptyObjects[newObjIdx]);
            }

            this.InsertedItemsIndexes = insertIndices;
            if (rebind)
            {
                DataBind();
                if (!ArrayUtil.IsNullOrEmpty(insertIndices))
                {
                    bool isPagingUsed = (this.grid.AllowPaging && this.grid.PageSize > 0);
                    int idxOfItemToFocus = isPagingUsed ? (insertIndices[0] % this.grid.PageSize) : insertIndices[0];
                    bool autoSelectedIfOnlyItemIsInserted = (insertIndices.Length == 1);
                    this.grid.FocusItem(insertIndices[0], autoSelectedIfOnlyItemIsInserted);
                }
            }
        }

        #endregion        

        #region Reset.

        /// <summary>
        /// Resets data about edited and inserted items which is kept in session variables and marks all bound objects as read-only.
        /// </summary>
        public void Reset()
        {
            Reset(true, true, true);
        }

        /// <summary>
        /// Resets data about edited and inserted items which is kept in session variables.
        /// </summary>
        /// <param name="markAllObjectsAsReadOnly">Whether to mark all business objects in the bound list as read-only (see <see cref="IValueObject.IsReadOnly"/>).</param>
        /// <param name="resetEditedItemsIndexes">Whether to reset data about edited items.</param>
        /// <param name="resetInsertedItemsIndexes">Whether to reset data about inserted items.</param>
        public void Reset(bool markAllObjectsAsReadOnly, bool resetEditedItemsIndexes, bool resetInsertedItemsIndexes)
        {
            if (markAllObjectsAsReadOnly)
            {
                foreach (ISelfValidatingObject bizObject in this.dataSource)
                    bizObject.IsReadOnly = true;
            }

            if (resetEditedItemsIndexes)
                this.EditedItemsIndexes = null;

            if (resetInsertedItemsIndexes)
                this.InsertedItemsIndexes = null;
        }

        #endregion

        #region Remove.

        /// <summary>
        /// Removes all selected items and rebinds data.
        /// </summary>
        public void RemoveSelected()
        {
            RemoveSelected(true);
        }

        /// <summary>
        /// Removes all selected items.
        /// </summary>
        /// <param name="rebind">Whether to rebind the changed datasource after the operation is completed.</param>
        public void RemoveSelected(bool rebind)
        {
            int[] rowIndexes = GetSelectedBusinessObjectIndexes();
            if (ArrayUtil.IsNullOrEmpty(rowIndexes))
                return;

            Remove(false, rowIndexes);
            this.grid.ClearSelectedValues();
            if (rebind)
                DataBind();
        }

        /// <summary>
        /// Removes objects at the specified indexes and rebinds data.
        /// </summary>
        /// <param name="indexes">The zero-based indexes of objects to remove.</param>
        public void Remove(params int[] indexes)
        {
            Remove(true, indexes);
        }

        /// <summary>
        /// Removes objects at the specified indexes.
        /// </summary>
        /// <param name="rebind">Whether to rebind the changed datasource after the operation is completed.</param>
        /// <param name="indexes">The zero-based indexes of objects to remove.</param>
        public void Remove(bool rebind, params int[] indexes)
        {
            ValidateIndexes(indexes);
            List<int> newEditedItemsIndexes = ArrayUtil.GetDifference<List<int>, int>(this.EditedItemsIndexes, indexes);
            this.EditedItemsIndexes = newEditedItemsIndexes.ToArray();
            List<int> newInsertedItemsIndexes = ArrayUtil.GetDifference<List<int>, int>(this.InsertedItemsIndexes, indexes);
            this.InsertedItemsIndexes = newInsertedItemsIndexes.ToArray();
            ArrayUtil.RemoveAt(this.dataSource, indexes);

            if (rebind)
                DataBind();
        }

        private void ValidateIndexes(int[] indices)
        {
            foreach (int idx in indices)
            {
                if (idx < 0 || idx >= this.dataSource.Count)
                    throw new ArgumentOutOfRangeException("Index out of range.");
            }
        }

        #endregion

        #region Accept.

        /// <summary>
        /// Specifies flags that control how the <b>AcceptChanges</b> method is conducted.
        /// </summary>
        [Flags]
        public enum AcceptFlags
        {
            #region Flags.

            /// <summary>
            /// Specifies no accept flag.
            /// </summary>
            Default = 0,

            /// <summary>
            /// Specifies that error description will always be modified so that the row number of the referenced 
            /// item is indicated. By default, descriptions are modified only if multiple object are edited at the same time.
            /// </summary>
            AlwaysIndicateInvalidRowNumbers = 1,

            /// <summary>
            /// Specifies that error description will always be modified so that the row number of the referenced 
            /// item is indicated. By default, descriptions are modified if multiple objects are edited at the same time.
            /// </summary>
            NeverIndicateInvalidRowNumbers = 2,

            /// <summary>
            /// Specifies that edited business objects will not be validated.
            /// </summary>
            DontValidateBusinessObject = 4,

            /// <summary>
            /// Specifies that parsing and validation errors will not be automatically displayed.
            /// </summary>
            DontDisplayErrors = 8,

            /// <summary>
            /// Specifies that the grid will remain in edit mode even if no parsing or validation errors have occured.
            /// </summary>
            AlwaysRemainInEditMode = 16,

            /// <summary>
            /// Specifies that the grid will not remain in edit mode even if parsing or validation errors have occured.
            /// </summary>
            NeverRemainInEditMode = 32,

            /// <summary>
            /// Specifies that the changed data will not be automatically bound to the grid.
            /// </summary>
            DontDataBind = 64,

            /// <summary>
            /// Specifies that encounterd parse error description will not be automatically built.
            /// </summary>
            DontBuildParseErrorDescriptions = 128

            #endregion
        }

        /// <summary>
        /// Accepts all changed data, stores it into business objects and performs validation.
        /// </summary>
        /// <param name="validationLevel">Indicates the level of validation.</param>
        /// <returns>Collection of all encountered parsing and validation errors.</returns>
        public BusinessRuleCollection AcceptChanges(ValidationLevel validationLevel)
        {
            return AcceptChanges(validationLevel, AcceptFlags.Default);
        }

        private int GetGridItemIndex(int dataItemIndex)
        {
            int rowIndex = -1;
            bool noPaging = !this.grid.AllowPaging || (this.grid.PageSize <= 0);
            if (noPaging)
            {
                rowIndex = dataItemIndex;
            }
            else
            {
                int dataItemPage = dataItemIndex / this.grid.PageSize;
                bool isDisplayed = (dataItemPage == this.grid.CurrentPageIndex);
                if (isDisplayed)
                    rowIndex = dataItemIndex % this.grid.PageSize;
            }

            return rowIndex;
        }

        /// <summary>
        /// Accepts all changed data, stores it into business objects and performs validation.
        /// </summary>
        /// <param name="validationLevel">Indicates the level of validation.</param>
        /// <param name="options">Specifies flags that control how the accept operation is conducted.</param>
        /// <returns>Collection of all encountered parsing and validation errors.</returns>
        public BusinessRuleCollection AcceptChanges(ValidationLevel validationLevel, AcceptFlags options)
        {
            int[] editIndices = this.EditedItemsIndexes;
            int[] insertIndices = this.InsertedItemsIndexes;
            int[] changedItemsIndexes = new int[editIndices.Length + insertIndices.Length];
            for (int editIdx = 0; editIdx < editIndices.Length; editIdx++)
                changedItemsIndexes[editIdx] = editIndices[editIdx];
            for (int insertIdx = 0; insertIdx < insertIndices.Length; insertIdx++)
                changedItemsIndexes[editIndices.Length + insertIdx] = insertIndices[insertIdx];

            bool invalidRowNumberIndicated = (changedItemsIndexes.Length > 1);
            if ((options & AcceptFlags.AlwaysIndicateInvalidRowNumbers) > 0)
                invalidRowNumberIndicated = true;
            if ((options & AcceptFlags.NeverIndicateInvalidRowNumbers) > 0)
                invalidRowNumberIndicated = false;

            bool dontDescribeParseErrors = ((options & AcceptFlags.DontBuildParseErrorDescriptions) > 0);

            bool hasErrors = false;
            BusinessRuleCollection[] multipleRowErrors = new BusinessRuleCollection[changedItemsIndexes.Length];
            for (int idxItem = 0; idxItem < multipleRowErrors.Length; idxItem++)
                multipleRowErrors[idxItem] = new BusinessRuleCollection();

            int itemCounter = 0;
            foreach (int idx in changedItemsIndexes)
            {
                // Skip items which were not rendered. This can only happen when paging is enabled.
                int rowIndex = GetGridItemIndex(idx);
                if (rowIndex < 0)
                    continue;

                BusinessRuleCollection rowErrors = new BusinessRuleCollection();

                // We need ISelfValidatingObject implementation/wrapper to perform validation and binding.
                ISelfValidatingObject changedObject = this.dataSource[idx] as ISelfValidatingObject;

                // Try to set modified by.
                IActor modifiedBy = UserSession.Actor;
                if (modifiedBy != null)
                {
                    if (changedObject is IDataObject)
                        (changedObject as IDataObject).SetModifiedBy(modifiedBy);
                }                

                // Parse errors.
                BusinessRuleCollection bindingEngineParseErrors = this.grid.PopulateBusinessObject(rowIndex, changedObject, 
                    this.defaultParseError, this.integerParseError, this.numberParseError, this.datetimeParseError);
                foreach (BusinessRule rule in bindingEngineParseErrors)
                {
                    rule.BusinessObject = changedObject;
                    bool describeParseError = !dontDescribeParseErrors
                        && rule.IsBroken
                        && string.IsNullOrEmpty(TextUtil.TrimNullableString(rule.Description))
                        && !string.IsNullOrEmpty(rule.ErrorCode)
                        && !ArrayUtil.IsNullOrEmpty(rule.AffectedFields);
                    if (describeParseError)
                    {
                        try
                        {
                            ParseError errorCode = (ParseError)Enum.Parse(typeof(ParseError), rule.ErrorCode);
                            rule.Description = BuildParseErrorDescription(rule.AffectedFields[0], errorCode);
                        }
                        catch { }
                    }
                }
                rowErrors.Add(bindingEngineParseErrors);

                // Business errors.
                bool dontValidate = ((options & AcceptFlags.DontValidateBusinessObject) > 0);
                if (!dontValidate)
                {
                    BusinessRuleCollection bizObjectValidationErrors = changedObject.Validate(validationLevel);
                    foreach (BusinessRule rule in bizObjectValidationErrors)
                        rule.BusinessObject = changedObject;

                    rowErrors.Add(bizObjectValidationErrors);
                }

                if (rowErrors.HasBrokenRules)
                    hasErrors = true;

                // Modify descriptions so that the no. of invalid items are indicated.
                // Future: invalid item decorator.
                if (invalidRowNumberIndicated)
                {
                    if (rowErrors.Count > 0)
                        rowErrors[0].Description = (idx + 1).ToString() + ". " + rowErrors[0].Description;
                    for (int indentedIdx = 1; indentedIdx < rowErrors.Count; indentedIdx++)
                        rowErrors[indentedIdx].Description = "    " + rowErrors[indentedIdx].Description;
                }

                multipleRowErrors[itemCounter] = rowErrors;
                itemCounter++;
            }

            bool dontDisplayErrors = ((options & AcceptFlags.DontDisplayErrors) > 0);
            if (hasErrors  && !dontDisplayErrors)
                this.grid.DisplayErrors(multipleRowErrors);

            bool remainInEditMode = hasErrors;
            if ((options & AcceptFlags.AlwaysRemainInEditMode) > 0)
                remainInEditMode = true;
            if ((options & AcceptFlags.NeverRemainInEditMode) > 0)
                remainInEditMode = false;

            if (!remainInEditMode)
            {
                // Accept all changes.
                foreach (ISelfValidatingObject bizObject in this.dataSource)
                    bizObject.IsReadOnly = true;

                this.EditedItemsIndexes = null;
                this.InsertedItemsIndexes = null;
            }

            if ((options & AcceptFlags.DontDataBind) == 0)
                DataBind();

            BusinessRuleCollection allErrors = new BusinessRuleCollection();
            foreach (BusinessRuleCollection itemErrors in multipleRowErrors)
            {
                if (!ArrayUtil.IsNullOrEmpty(itemErrors))
                    allErrors.Add(itemErrors);
            }

            return allErrors;
        }

        /// <summary>
        /// Accepts all changed data, stores it into business objects and performs validation
        /// using <see cref="ValidationLevel.RulesThatDontRequireDatabaseAccess"/> level.
        /// </summary>
        /// <returns><b>True</b> if all changed data is valid and the grid exited edit mode; 
        /// <b>false</b> if errors occured and the grid cannot exit edit mode.</returns>
        /// <remarks>Use this overload when editing a collection of business object and data-access
        /// is not required for validation at edit time. It is a good practice to perform a full scaled
        /// validation using <see cref="ValidationLevel.AllBusinessRules"/> level just before saving the data.</remarks>
        public bool AcceptChanges()
        {            
            int[] editIndices = this.EditedItemsIndexes;
            int[] insertIndices = this.InsertedItemsIndexes;
            int[] changedItemsIndexes = new int[editIndices.Length + insertIndices.Length];
            for (int editIdx = 0; editIdx < editIndices.Length; editIdx++)
                changedItemsIndexes[editIdx] = editIndices[editIdx];
            for (int insertIdx = 0; insertIdx < insertIndices.Length; insertIdx++)
                changedItemsIndexes[editIndices.Length + insertIdx] = insertIndices[insertIdx];

            bool invalidRowNumberIndicated = (changedItemsIndexes.Length > 1  &&  this.grid.RowSelectMode != BoundDataGrid.GridRowSelectMode.SingleRow);

            bool remainInEditMode = false;
            BusinessRuleCollection[] multipleRowErrors = new BusinessRuleCollection[changedItemsIndexes.Length];
            for (int idxItem = 0; idxItem < multipleRowErrors.Length; idxItem++)
                multipleRowErrors[idxItem] = new BusinessRuleCollection();

            int itemCounter = 0;
            foreach (int idx in changedItemsIndexes)
            {
                // Skip items which were not rendered. This can only happen when paging is enabled.
                int rowIndex = GetGridItemIndex(idx);
                if (rowIndex < 0)
                    continue;
                //int rowIndex = idx;

                BusinessRuleCollection rowErrors = new BusinessRuleCollection();

                // We need ISelfValidatingObject implementation/wrapper to perform validation and binding.
                ISelfValidatingObject changedObject = this.dataSource[idx] as ISelfValidatingObject;
                BusinessRuleCollection bindingEngineParseErrors = this.grid.PopulateBusinessObject(rowIndex, changedObject, 
                    this.defaultParseError, this.integerParseError, this.numberParseError,this.datetimeParseError);
                foreach (BusinessRule rule in bindingEngineParseErrors)
                {
                    rule.BusinessObject = changedObject;
                    if (rule.IsBroken && !string.IsNullOrEmpty(rule.ErrorCode) && rule.AffectedFields != null && rule.AffectedFields.Length > 0)
                    {
                        try
                        {
                            ParseError errorCode = (ParseError)Enum.Parse(typeof(ParseError), rule.ErrorCode);
                            rule.Description = BuildParseErrorDescription(rule.AffectedFields[0], errorCode);
                        }
                        catch { }
                    }
                }
                rowErrors.Add(bindingEngineParseErrors);

                BusinessRuleCollection bizObjectValidationErrors = changedObject.Validate(ValidationLevel.RulesThatDontRequireDatabaseAccess);
                rowErrors.Add(bizObjectValidationErrors);

                if (rowErrors.HasBrokenRules)
                    remainInEditMode = true;

                if (invalidRowNumberIndicated)
                {
                    if (rowErrors.Count > 0)
                        rowErrors[0].Description = (idx + 1).ToString() + ". " + rowErrors[0].Description;
                    for (int indentedIdx = 1; indentedIdx < rowErrors.Count; indentedIdx++)
                        rowErrors[indentedIdx].Description = "    " + rowErrors[indentedIdx].Description;
                }

                multipleRowErrors[itemCounter] = rowErrors;
                itemCounter++;
            }

            if (remainInEditMode)
            {
                this.grid.DisplayErrors(multipleRowErrors);
            }
            else
            {
                // Accept all changes.
                foreach (ISelfValidatingObject bizObject in this.dataSource)
                    bizObject.IsReadOnly = true;

                this.EditedItemsIndexes = null;
                this.InsertedItemsIndexes = null;
            }

            DataBind();
            bool isSuccess = !remainInEditMode;
            return isSuccess;
        }

        private string BuildParseErrorDescription(string fieldCaption, ParseError errorCode)
        {
            // If dedicated error description is not available, return default description.

            StringBuilder error = new StringBuilder();
            string description;
            switch (errorCode)
            {
                case ParseError.WrongDateFormat:
                    description = (this.datetimeParseError != null) ? this.datetimeParseError : this.defaultParseError;
                    break;

                case ParseError.WrongIntegerFormat:
                    description = (this.integerParseError != null) ? this.integerParseError : this.defaultParseError;
                    break;

                case ParseError.WrongNumberFormat:
                    description = (this.numberParseError != null) ? this.numberParseError : this.defaultParseError;
                    break;

                default:
                    description = this.defaultParseError;
                    break;
            }

            if (description != null && description.Length > 0)
            {
                if (fieldCaption != null && fieldCaption.Length > 0)
                {
                    error.Append(char.ToUpper(fieldCaption[0]));
                    if (fieldCaption.Length > 1)
                        error.Append(fieldCaption.Substring(1));
                    error.Append(" - ");
                    error.Append(char.ToLower(description[0]));
                    if (description.Length > 1)
                        error.Append(description.Substring(1));
                    if (error[error.Length - 1] != '.')
                        error.Append('.');
                }
                else
                {
                    error.Append(char.ToUpper(description[0]));
                    if (description.Length > 1)
                        error.Append(description.Substring(1));
                    if (error[error.Length - 1] != '.')
                        error.Append('.');
                }
            }

            return error.ToString();
        }

        #endregion

        #region Reject.

        /// <summary>
        /// Rejects all changed data. Business objects are not changed.
        /// </summary>
        public void RejectChanges()
        {
            // Remove inserted items. From highest to lowest index.
            int[] insertedItemsIndexes = this.InsertedItemsIndexes;
            Array.Sort(insertedItemsIndexes);
            for (int idxOfIndexToBeRemoved = insertedItemsIndexes.Length - 1; idxOfIndexToBeRemoved >= 0; idxOfIndexToBeRemoved--)
                this.dataSource.RemoveAt(insertedItemsIndexes[idxOfIndexToBeRemoved]);

            foreach (ISelfValidatingObject bizObject in this.dataSource)
                bizObject.IsReadOnly = true;

            this.InsertedItemsIndexes = null;
            this.EditedItemsIndexes = null;
            DataBind();
        }

        #endregion

        #region Parse error messages.

        /// <summary>
        /// Gets or sets the generic message displayed when a parsing error occures.
        /// </summary>
        public string DefaultParseError
        {
            get { return this.defaultParseError; }
            set { this.defaultParseError = value; }
        }

        /// <summary>
        /// Gets or sets the generic message displayed when parsing of integer value fails.
        /// </summary>
        public string IntegerParseError
        {
            get { return this.integerParseError; }
            set { this.integerParseError = value; }
        }

        /// <summary>
        /// Gets or sets the generic message displayed when number parsing fails.
        /// </summary>
        public string NumberParseError
        {
            get { return this.numberParseError; }
            set { this.numberParseError = value; }
        }

        /// <summary>
        /// Gets or sets the generic message displayed when parsing of DateTime value fails.
        /// </summary>
        public string DatetimeParseError
        {
            get { return this.datetimeParseError; }
            set { this.datetimeParseError = value; }
        }

        #endregion

        #region Properties.

        /// <summary>
        /// Gets value indicating whether the data has been bound to the DataGrid.
        /// </summary>
        public bool IsDataBound
        {
            get { return this.dataIsBound; }
        }

        #endregion
    }
}
